#include "VideoHardware.h"
#include "Common.h"

#include <memory>
#include <cassert>
#include <cstring>

#define VIDEO_MEMORY 0x2000 + 0x20 + 0x100

VideoHardware::VideoHardware(const byte_t* const videoMemory, const byte_t* const spriteMemory, const byte_t* const spriteCoordsMemory)
	: m_videoMemory(videoMemory)
	, m_colorMemory(videoMemory + 0x400)
	, m_spriteMemory(spriteMemory)
	, m_spriteCoordsMemory(spriteCoordsMemory)
{
	m_memory = new byte_t[VIDEO_MEMORY];

	m_charactersDef					= m_memory + 0x0000;
	m_spritesDef					= m_memory + 0x1000;
	m_colorsPalette					= m_memory + 0x2000;
	m_charactersAndSpritesColors	= m_memory + 0x2020;

	LoadRomFile("rom/pacman.5e", m_charactersDef, 0x1000);
	LoadRomFile("rom/pacman.5f", m_spritesDef	, 0x1000);

	LoadRomFile("rom/82s123.7f", m_colorsPalette, 0x20);
	LoadRomFile("rom/82s126.4a", m_charactersAndSpritesColors, 0x100);

	const size_t outputImageSize = video_width * video_height * 3; // RGB = 3byte for pixel 
	m_outputImage = new byte_t[outputImageSize];
}

VideoHardware::~VideoHardware()
{
	delete[] m_memory;
	delete[] m_outputImage;
}

void VideoHardware::DrawCharacters() const
{
	ProcessRow(1, 0x02, 0x01E);
	ProcessRow(0, 0x22, 0x03E);

	ProcessColumns(0x40, 0x3C0);

	ProcessRow(35, 0x3C2, 0x3DE);
	ProcessRow(34, 0x3E2, 0x3FE);
}

void VideoHardware::DrawSprites() const
{
	// sprites are drawn in reverse order
	for(int i = 8; i > 0; --i)
	{
		size_t j = i * 2;
		byte_t data		= m_spriteMemory[j];
		byte_t color	= m_spriteMemory[j + 1];
		
		byte_t shape	= data >> 2;
		byte_t mode		= data & 3;
		
		byte_t x		= m_spriteCoordsMemory[j];
		byte_t y		= m_spriteCoordsMemory[j + 1];

		DrawSprite(shape, mode, color, x + 1, y); // it's + 1 because it just works :3
	}	
}

void VideoHardware::DrawSprite(byte_t shape, byte_t mode, byte_t color, word_t xCoord, word_t yCoord) const
{
	if(yCoord < 16)
	{
		yCoord += 256;
	}
	if(xCoord < 31 || xCoord > 239)
		return;

	bool isFlippedX = (mode & 2) > 0;
	bool isFlippedY = (mode & 1) > 0;

	size_t outImageOffset = (-xCoord*3) + (16 * 3) + (yCoord * video_width * 3);
	byte_t * outPixels = m_outputImage + outImageOffset;

	byte_t * spritesDef = m_spritesDef + shape * 64;

	if (isFlippedY)
	{
		DrawCharacterHelper(spritesDef + 0,  color, outPixels + 24, 12, isFlippedX, isFlippedY);
		DrawCharacterHelper(spritesDef + 24, color, outPixels + 24, 8,	isFlippedX, isFlippedY);
		DrawCharacterHelper(spritesDef + 16, color, outPixels + 24, 4,	isFlippedX, isFlippedY);
		DrawCharacterHelper(spritesDef + 8,  color, outPixels + 24, 0,	isFlippedX, isFlippedY);

		DrawCharacterHelper(spritesDef + 32, color, outPixels, 12,		isFlippedX, isFlippedY);
		DrawCharacterHelper(spritesDef + 56, color, outPixels, 8,		isFlippedX, isFlippedY);
		DrawCharacterHelper(spritesDef + 48, color, outPixels, 4,		isFlippedX, isFlippedY);
		DrawCharacterHelper(spritesDef + 40, color, outPixels, 0,		isFlippedX, isFlippedY);
	}
	else
	{
		if (isFlippedX)
		{
			DrawCharacterHelper(spritesDef + 8,  color, outPixels, 12,  isFlippedX, isFlippedY);
			DrawCharacterHelper(spritesDef + 16, color, outPixels, 8,	isFlippedX, isFlippedY);
			DrawCharacterHelper(spritesDef + 24, color, outPixels, 4,	isFlippedX, isFlippedY);
			DrawCharacterHelper(spritesDef + 0,  color, outPixels, 0,	isFlippedX, isFlippedY);

			DrawCharacterHelper(spritesDef + 40, color, outPixels + 24, 12,	isFlippedX, isFlippedY);
			DrawCharacterHelper(spritesDef + 48, color, outPixels + 24, 8,	isFlippedX, isFlippedY);
			DrawCharacterHelper(spritesDef + 56, color, outPixels + 24, 4,	isFlippedX, isFlippedY);
			DrawCharacterHelper(spritesDef + 32, color, outPixels + 24, 0,	isFlippedX, isFlippedY);
		}
		else
		{
			DrawCharacterHelper(spritesDef + 8,  color, outPixels + 24, 12, isFlippedX, isFlippedY);
			DrawCharacterHelper(spritesDef + 16, color, outPixels + 24, 8,	isFlippedX, isFlippedY);
			DrawCharacterHelper(spritesDef + 24, color, outPixels + 24, 4,	isFlippedX, isFlippedY);
			DrawCharacterHelper(spritesDef + 0,  color, outPixels + 24, 0,	isFlippedX, isFlippedY);

			DrawCharacterHelper(spritesDef + 40, color, outPixels, 12,		isFlippedX, isFlippedY);
			DrawCharacterHelper(spritesDef + 48, color, outPixels, 8,		isFlippedX, isFlippedY);
			DrawCharacterHelper(spritesDef + 56, color, outPixels, 4,		isFlippedX, isFlippedY);
			DrawCharacterHelper(spritesDef + 32, color, outPixels, 0,		isFlippedX, isFlippedY);
		}
	}
}

void VideoHardware::DrawCharacter(byte_t characterIndex, byte_t baseColor, byte_t* outPixels) const
{
	byte_t * charactersDefTmp = m_charactersDef + 16 * characterIndex;

	DrawCharacterHelper(charactersDefTmp + 8, baseColor, outPixels, 4);
	DrawCharacterHelper(charactersDefTmp, baseColor, outPixels, 0);
}

void VideoHardware::DrawCharacterHelper(byte_t * dataPtr, byte_t baseColor, byte_t * outPixels, size_t count, bool flipX, bool flipY) const
{
	for (size_t i = 0; i < 8; ++i)
	{
		byte_t data = *dataPtr++;

		size_t offset[4];
		if (flipX)
		{
			offset[0] = i*3 + (count+0)*video_width*3;
			offset[1] = i*3 + (count+1)*video_width*3;
			offset[2] = i*3 + (count+2)*video_width*3;
			offset[3] = i*3 + (count+3)*video_width*3;
		}
		else
		{
			offset[0] = (7-i)*3 + (count+0)*video_width*3;
			offset[1] = (7-i)*3 + (count+1)*video_width*3;
			offset[2] = (7-i)*3 + (count+2)*video_width*3;
			offset[3] = (7-i)*3 + (count+3)*video_width*3;
		}

		byte_t color01_pi, color02_pi, color03_pi, color04_pi;

		if (flipY)
		{
			color04_pi = ((data & 0x10) >> 3 ) | ((data & 0x01) >> 0);
			color03_pi = ((data & 0x20) >> 4 ) | ((data & 0x02) >> 1);
			color02_pi = ((data & 0x40) >> 5 ) | ((data & 0x04) >> 2);
			color01_pi = ((data & 0x80) >> 6 ) | ((data & 0x08) >> 3);
		}
		else
		{
			color01_pi = ((data & 0x10) >> 3 ) | ((data & 0x01) >> 0);
			color02_pi = ((data & 0x20) >> 4 ) | ((data & 0x02) >> 1);
			color03_pi = ((data & 0x40) >> 5 ) | ((data & 0x04) >> 2);
			color04_pi = ((data & 0x80) >> 6 ) | ((data & 0x08) >> 3);
		}

		byte_t color01 = (baseColor << 2) | (color01_pi); 
		byte_t color02 = (baseColor << 2) | (color02_pi); 
		byte_t color03 = (baseColor << 2) | (color03_pi); 
		byte_t color04 = (baseColor << 2) | (color04_pi); 

		color01 = m_colorsPalette[m_charactersAndSpritesColors[color01]];
		color02 = m_colorsPalette[m_charactersAndSpritesColors[color02]];
		color03 = m_colorsPalette[m_charactersAndSpritesColors[color03]];
		color04 = m_colorsPalette[m_charactersAndSpritesColors[color04]];

		if (color01 > 0x00)
		{
			outPixels[offset[0]+0] = (byte_t)(((float)(color01 & 0x07) / (float)0x07) * 0xFF);
			outPixels[offset[0]+1] = (byte_t)(((float)((color01 & 0x38) >> 3) / (float)0x07) * 0xFF);
			outPixels[offset[0]+2] = (byte_t)(((float)((color01 & 0xC0) >> 6) / (float)0x04) * 0xFF);
		}
		
		if (color02 > 0x00)
		{
			outPixels[offset[1]+0] = (byte_t)(((float)(color02 & 0x07) / (float)0x07) * 0xFF);
			outPixels[offset[1]+1] = (byte_t)(((float)((color02 & 0x38) >> 3) / (float)0x07) * 0xFF);
			outPixels[offset[1]+2] = (byte_t)(((float)((color02 & 0xC0) >> 6) / (float)0x04) * 0xFF);
		}

		if (color03 > 0x00)
		{
			outPixels[offset[2]+0] = (byte_t)(((float)(color03 & 0x07) / (float)0x07) * 0xFF);
			outPixels[offset[2]+1] = (byte_t)(((float)((color03 & 0x38) >> 3) / (float)0x07) * 0xFF);
			outPixels[offset[2]+2] = (byte_t)(((float)((color03 & 0xC0) >> 6) / (float)0x04) * 0xFF);
		}

		if (color04 > 0x00)
		{
			outPixels[offset[3]+0] = (byte_t)(((float)(color04 & 0x07) / (float)0x07) * 0xFF);
			outPixels[offset[3]+1] = (byte_t)(((float)((color04 & 0x38) >> 3) / (float)0x07) * 0xFF);
			outPixels[offset[3]+2] = (byte_t)(((float)((color04 & 0xC0) >> 6) / (float)0x04) * 0xFF);
		}
	}
}

void VideoHardware::ProcessRow(byte_t rowIndex, word_t start, word_t end) const
{
	size_t outImageOffset = (27 * 8 * 3) + (rowIndex * 8 * video_width * 3);

	word_t offset = start;
	size_t count(0);

	while(offset < end)
	{
		const byte_t characterDefinitionIndex	= m_videoMemory[offset];
		const byte_t baseColor					= m_colorMemory[offset];
		DrawCharacter(characterDefinitionIndex, baseColor, m_outputImage + outImageOffset - count * 3 * 8);
		++offset; ++count;
	}
}

void VideoHardware::ProcessColumns(word_t start, word_t end) const
{
	int columnIndex(27);
	int rowIndex(33);

	word_t offset = start;

	while(offset < end)
	{
		size_t outImageOffset = (columnIndex * 8 * 3) + (rowIndex * 8 * video_width * 3);

		const byte_t characterDefinitionIndex	= m_videoMemory[offset];
		const byte_t baseColor					= m_colorMemory[offset];
		DrawCharacter(characterDefinitionIndex, baseColor, m_outputImage + outImageOffset);

		++offset; --rowIndex;

		if(rowIndex == 1)
		{
			rowIndex = 33;
			--columnIndex;
		}
	}
}

void VideoHardware::Update() const
{
	memset(m_outputImage, 0, video_width * video_height * 3);

	DrawCharacters();
	DrawSprites();
}
